﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.DataSourcesGDB;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Carto;
using ESRI.ArcGIS.ArcMap;
using ESRI.ArcGIS.ArcMapUI;
using ESRI.ArcGIS.ADF;
using ESRI.ArcGIS.Framework;
using ESRI.ArcGIS.Geometry;

namespace BesAsm.Framework.ArcGisUtilities
{
  /// <summary>
  /// Contains extension methods for the various interfaces implemented by the ArcGIS FeatureLayer CoClass, 
  /// such as IFeatureLayer, IFeatureLayerDefinition and IFeatureSelection.
  /// </summary>
  public static class FeatureLayerExtensions
  {
    public static IFeatureLayer EmptyLayer(this IFeatureLayerDefinition layer, string newLayerName, IApplication arcApp)
    {
      IMxDocument doc = (IMxDocument)arcApp.Document;
      ArcGisUtils arcUtils = new ArcGisUtils(arcApp);
      arcUtils.RemoveLayers(newLayerName);

      IFeatureLayer fl = layer.CreateSelectionLayer(newLayerName, false, null, "false");

      return fl;
    }

    /// <summary>
    /// Creates a new layer from the selected elements in this ArcList object's
    /// _FeatureLayer. Any existing layers with the same name as the new layer will be removed.
    /// The new layer is automatically added to the active map, and the 
    /// IActiveViewEvent.OnItemAdded event will be raised.</summary>
    /// <param name="newLayerName">The name of the new layer to be created</param>
    /// <returns>A reference to the newly created layer</returns>
    public static IFeatureLayer NewLayerFromSelection(this IFeatureLayerDefinition layer, string newLayerName, IApplication arcApp)
    {
      IMxDocument doc = (IMxDocument)arcApp.Document;
      ArcGisUtils arcUtils = new ArcGisUtils(arcApp);
      arcUtils.RemoveLayers(newLayerName);

      IFeatureLayer fl = layer.CreateSelectionLayer(newLayerName, true, null, null);      
      return fl;
    }

    /// <summary>
    /// Creates a new layer from the selected elements in this ArcList object's
    /// _FeatureLayer. Any existing layers with the same name as the new layer will be removed.
    /// The new layer is automatically added to the active map, and the 
    /// IActiveViewEvent.OnItemAdded event will be raised.
    /// </summary>
    /// <param name="newLayerName">The name of the new layer to create</param>
    /// <param name="groupName">The name of a group layer to add the new layer to. The group
    /// layer will be created if it does not exist.</param>
    /// <returns>A reference to the newly created layer</returns>
    public static IFeatureLayer NewLayerFromSelection(this IFeatureLayerDefinition layer, string newLayerName, string groupName, IApplication arcApp)
    {
      ArcGisUtils arcUtils = new ArcGisUtils(arcApp);
      arcUtils.RemoveLayers(newLayerName);

      IFeatureLayer fl = layer.NewLayerFromSelection(newLayerName, arcApp);
      arcUtils.MoveLayerToGroup(fl, groupName);
      return fl;
    }

    /// <summary>
    /// Selects a list of features according to the ArcGIS Object Id field
    /// </summary>        
    /// <param name="ids">An array of Object Id values to select</param>
    /// <param name="clearSelection">True to clear the existing selection set before adding new selection</param>
    public static void SelectFeatures(this IFeatureLayer layer, int[] oids, bool clearSelection)
    {
      IFeatureSelection fs = (IFeatureSelection)layer;

      if (oids.Length == 0)
        return;

      if (clearSelection)
      {
        fs.Clear();
      }

      fs.SelectionSet.Refresh();

      fs.SelectionSet.AddList(oids.Length, ref oids[0]);
    }

    /// <summary>
    /// Gets the an array of ObjectIds from the features which are selected in the ArcGIS UI.
    /// </summary>
    /// <param name="layer">The layer.</param>
    /// <returns>An array of ObjectIds</returns>
    public static int[] GetSelectedOids(this IFeatureLayer layer)
    {
      List<int> oids = new List<int>();
      ICursor cursor;

      IFeatureSelection fs = (IFeatureSelection)layer;
      fs.SelectionSet.Search(null, false, out cursor);

      IRow row = cursor.NextRow();
      while (row != null)
      {
        oids.Add(row.OID);
        row = cursor.NextRow();
      }
      return oids.ToArray();
    }

    /// <summary>
    /// Gets an array of all the ObjectIds in this layer.
    /// </summary>
    /// <param name="layer">The layer.</param>
    /// <returns></returns>
    public static int[] GetAllOids(this IFeatureLayer layer)
    {
      List<int> oids = new List<int>();
      try
      {
        if (layer.FeatureClass.FeatureCount(null) > 0)
        {
          IFeatureCursor cursor = layer.Search(null, false);
          IFeature feature = cursor.NextFeature();

          while (feature != null)
          {
            oids.Add(feature.OID);
            feature = cursor.NextFeature();
          }
        }
      }
      catch (Exception ex)
      {
        // Don't do anything; a COMException is thrown if the layer is empty
      }

      return oids.ToArray();
    }

    /// <summary>
    /// Clears the selection.
    /// </summary>
    /// <param name="layer">The layer.</param>
    public static void ClearSelection(this IFeatureSelection layer)
    {
      layer.Clear();
    }

    /// <summary>
    /// Redraws the specified layer.
    /// </summary>
    /// <param name="layer">The layer.</param>
    /// <param name="arcApp">The arc app.</param>
    public static void Redraw(this IFeatureLayer layer, IApplication arcApp)
    {
      IMxDocument doc = arcApp.Document as IMxDocument;
      IActiveView view = (IActiveView)doc.FocusMap;
      view.Refresh();
    }

    /// <summary>
    /// Applies symbology from a layer file.
    /// </summary>
    /// <param name="layer">The layer.</param>
    /// <param name="lyrFile">The lyr file.</param>
    public static void ApplyLayerFile(this IGeoFeatureLayer layer, string lyrFile)
    {
      if (!System.IO.File.Exists(lyrFile))
        throw new ArgumentException("Can not apply symbology: File '" + lyrFile + "' not found.");

      ILayerFile layerFile = new LayerFileClass();
      layerFile.Open(lyrFile);

      IGeoFeatureLayer inLayer = layerFile.Layer as IGeoFeatureLayer;
      IFeatureRenderer inRenderer = inLayer.Renderer;
      layerFile.Close();

      IObjectCopy copy = new ObjectCopyClass();
      layer.Renderer = copy.Copy(inRenderer) as IFeatureRenderer;
    }

    /// <summary>
    /// Saves to this layer to a personal geodatabase.
    /// </summary>
    /// <param name="layer">This layer.</param>
    /// <param name="name">The name of this saved layer in the geodatabase</param>
    /// <param name="path">The path to the geodatabase</param>
    public static void SaveSelectionToPgdb(this IDataset layer, string name, string path)
    {
      IDatasetName inputName = (layer.FullName) as IDatasetName;

      ILayerFile layerFile = new LayerFileClass();
      IWorkspaceFactory wsf = new AccessWorkspaceFactory();

      IWorkspace ws = wsf.OpenFromFile(path, 0);
      IDataset ds = ws as IDataset;

      IDatasetName outputName = new FeatureClassNameClass();
      outputName.Name = name;
      outputName.WorkspaceName = (ds.FullName as IWorkspaceName);

      IFeatureDataConverter2 fdc = new FeatureDataConverterClass();
      IFeatureSelection fs = (IFeatureSelection)layer;

      try
      {
        ws.DeleteFeatureClass(name);
        fdc.ConvertFeatureClass(inputName, null, fs.SelectionSet, null, outputName as IFeatureClassName, null, null, "", 100, 0);
      }
      catch (Exception ex)
      {
        //TODO: Occasionally getting unexplained exceptions here, monitor this to see if repeatable.
        throw new System.IO.IOException("Unable to write FeatureClass '" + name + "' to geodatabase at '" + path + "'", ex);
      }
      finally
      {
        System.Runtime.InteropServices.Marshal.FinalReleaseComObject(ws);
        wsf = null;
        ds = null;
        outputName = null;
      }

    }

    /// <summary>
    /// Calculates the overlap area between features in two FeatureClass.
    /// </summary>
    /// <param name="fc">The base FeatureClass</param>
    /// <param name="overlapFc">The overlap FeatureClass</param>
    /// <returns>
    /// A nested dictionary used the base FeatureClass ObjectId as the first key, the overlap FeatureClass ObjectId as the
    /// nested key, and the fraction of overlap as the nested value.
    /// </returns>
    public static Dictionary<int, Dictionary<int, double>> CalculateOverlap(this IFeatureClass fc, IFeatureClass overlapFc)
    {
      Dictionary<int, Dictionary<int, double>> results = new Dictionary<int, Dictionary<int, double>>();
      Dictionary<int, double> overlappers = new Dictionary<int, double>();

      IFeatureCursor thisFeatureCursor;
      IFeature thisFeature;

      IFeatureCursor overlapCursor;
      IFeature overlapFeature;
      IArea overlapFeatureArea;

      IArea overlappingArea;

      ISpatialFilter spatialFilter;
      IRelationalOperator relationalOperator;
      ITopologicalOperator2 topOp;

      thisFeatureCursor = fc.Search(null, false);
      thisFeature = thisFeatureCursor.NextFeature();

      double overlap;

      try
      {
        while (thisFeature != null)
        {
          spatialFilter = new SpatialFilterClass();
          spatialFilter.Geometry = thisFeature.Shape;
          spatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects;
          spatialFilter.GeometryField = fc.ShapeFieldName;

          relationalOperator = (IRelationalOperator)thisFeature.Shape;

          overlapCursor = overlapFc.Search(spatialFilter, false);
          overlapFeature = overlapCursor.NextFeature();

          topOp = (ITopologicalOperator2)thisFeature.ShapeCopy;

          #region ITopologicalOperator2 stability fixes
          /* ITopologicalOperator2 can be unstable.
           * Additional potential fixes documented here:
           * http://forums.esri.com/Thread.asp?c=93&f=1170&t=88297
           * 
           */
          topOp.IsKnownSimple_2 = false;
          topOp.Simplify();
          #endregion

          overlappers = new Dictionary<int, double>();
          overlap = 0;
          while (overlapFeature != null)
          {
            overlappingArea = (IArea)topOp.Intersect(overlapFeature.ShapeCopy, esriGeometryDimension.esriGeometry2Dimension);
            overlapFeatureArea = (IArea)overlapFeature.Shape;

            overlap = overlappingArea.Area / overlapFeatureArea.Area;
            overlappers.Add(overlapFeature.OID, overlap);

            overlapFeature = overlapCursor.NextFeature();
          }

          overlapCursor = null;
          GC.Collect();

          results.Add(thisFeature.OID, overlappers);

          thisFeature = thisFeatureCursor.NextFeature();
        }
      }
      catch (Exception ex)
      {
        // TODO: Partial results returned, need to handle this outside
        throw ex;
      }
      return results;

    }

  }
}
